package sim_moteur;

import java.util.Stack;

import sim_moteur.element.Chose;
import sim_moteur.element.vegetal.Vegetal;
import util.RandomSansRepetition;
import util.Sequence;
import util.SimProperties;

public class Terrain {

	private static final int NOMBRE_DE_RECHERCHE_SEQUENCE = 15;
	public final static int SIZE_X = 15;
	public final static int SIZE_Y = 15;
	public final static String PROPERTY_DIMENSION_Y_NAME = "simulation.terrain.y.dimension";
	public final static String PROPERTY_DIMENSION_X_NAME = "simulation.terrain.x.dimension";

	private Stack<Chose>[][] lieux;
	private static Terrain singleton = new Terrain();

	private Terrain() {
		init();
	}

	@SuppressWarnings("unchecked")
	public void init() {
		int sizeX = SIZE_X;
		int sizeY = SIZE_Y;

		SimProperties props = SimProperties.getInstance();
		int xSizeProp = props.getIntProperty(PROPERTY_DIMENSION_X_NAME);
		int ySizeProp = props.getIntProperty(PROPERTY_DIMENSION_Y_NAME);
		if (xSizeProp != -1)
			sizeX = xSizeProp;
		if (ySizeProp != -1)
			sizeY = ySizeProp;
		lieux = new Stack[sizeX][sizeY];

		for (int i = 0; i < lieux.length; i++)
			for (int j = 0; j < lieux[0].length; j++) {
				lieux[j][i] = new Stack<Chose>();
			}
	}

	/**
	 * retourne l'instance de cette classe (singleton)
	 * 
	 * @return l'instance
	 */
	public static Terrain getInstance() {
		return singleton;
	}

	/**
	 * Retourne le nombre de colonnes.
	 * 
	 * @return la dimension x
	 */
	public int getXSize() {
		return lieux.length;
	}

	/**
	 * Retourne le nombre de rangées.
	 * 
	 * @return la dimension y
	 */
	public int getYSize() {
		return lieux[0].length;
	}

	/**
	 * Retourne le dernier élément placé sur le terrain à l'emplacement (x,y).
	 * Retourne null s'il n'y pas d'élément à cet endroit
	 * 
	 * @param x
	 *            la coordonnée x
	 * @param y
	 *            la coordonnée y
	 * @return Le dernier élément
	 */
	public Stack<Chose> get(int x, int y) {
		return lieux[x][y];
	}

	@SuppressWarnings("unchecked")
	public void clear() {
		lieux = new Stack[lieux[0].length][lieux.length];

		for (int i = 0; i < lieux.length; i++)
			for (int j = 0; j < lieux[0].length; j++) {
				lieux[j][i] = new Stack<Chose>();
			}
	}

	/**
	 * Déplace un élément sur le terrain. Si l'élément peut être placé à
	 * l'endroit demandé.(Méthode asynchrone)
	 * 
	 * @param item
	 *            l'élément àplacer
	 * @param nouvelleCoordonnee
	 *            L'endroit où l'on veut déplacer l'élément.
	 * @return vrai si l'élément a été déplacé.
	 */
	public boolean deplace(Chose item, Coordonnee nouvelleCoordonnee) {

		// puis on le place ailleurs.
		int x = item.getCoordonnees().getX();
		int y = item.getCoordonnees().getY();
		boolean estPlace = place(item, nouvelleCoordonnee);
		// On enlève l'item du terrain
		if (estPlace) {
			synchronized (lieux[x][y]) {
				lieux[x][y].remove(item);
			}
		}
		return estPlace;

	}

	/**
	 * Essaie de placer une élément à un endroit (donnée par la coordonnée reçue
	 * en paramètre). Si l'emplacement est déjà occupé, la méthode recoit() de
	 * la classe chose est utilisée pour vérifier si l'élément déjà présent peut
	 * recevoir le nouvel élément (une plante peut recevoir un animal). Lorsque
	 * la méthode place un élément sur le terrain, elle ajuste ses coordonnées
	 * pour refléter sa position. (Méthode asynchrone)
	 * 
	 * @param item
	 *            l'élément à placer sur le terrain.
	 * @param coordonnée
	 *            l'endroit où l'on veut pacer la chose.
	 * @return vrai si la chose a été placé à l'endroit demandée
	 */

	public boolean place(Chose item, Coordonnee coordonnee) {
		int x = coordonnee.getX();
		int y = coordonnee.getY();
		boolean retVal = false;

		if (x < Terrain.getInstance().getXSize() && x >= 0 && y >= 0
				&& y < Terrain.getInstance().getYSize()) {
			synchronized (lieux[x][y]) {
				Stack<Chose> lieu = lieux[x][y];
				// S'il n'y a rien sur ce lieu
				if (lieu.isEmpty()) {
					lieu.push(item);
					retVal = true;
				} else {
					// Si la chose présente accepte qu'on y ajoute la
					// nouvelle
					// chose
					// ex: une plante peut partager son emplacement avec un
					// animal
					if (lieu.peek().recoit(item)) {
						lieu.push(item);
						retVal = true;
					}
				}
			}
		}

		if (retVal)
			item.setCoordonnees(coordonnee);
		return retVal;
	}

	/**
	 * Idinque si l'on peut placer la chose reçue à la coordonnée reçue.
	 * 
	 * @param item
	 *            La chose à placer
	 * @param coordonnee
	 *            L'endroit où placer la chose
	 * @return vrai si la chose peut être placée.
	 */
	public boolean estPlacable(Chose item, Coordonnee coordonnee) {
		int x = coordonnee.getX();
		int y = coordonnee.getY();

		boolean retVal = false;
		if (x < Terrain.getInstance().getXSize() && x >= 0 && y >= 0
				&& y < Terrain.getInstance().getYSize()) {
			Stack<Chose> lieu = lieux[x][y];

			// Si la chose présente accepte qu'on y ajoute la nouvelle chose
			// ex: une plante peut partager son emplacement avec un animal
			if (lieu.isEmpty() || lieu.peek().recoit(item)) {
				retVal = true;
			}
		}

		return retVal;
	}

	/**
	 * Retourne le vegetal qui se trouve à la coordonnée reçue en paramètre s'il
	 * y a un végétal à cet endroit.
	 * 
	 * @param coordonnees
	 *            L'endroit où chercher le vegetal.
	 * @return vrai s'il y a un vegetal.
	 */
	public Vegetal trouveUnVegetal(Coordonnee coordonnees) {
		int x = coordonnees.getX();
		int y = coordonnees.getY();

		Vegetal retVal = null;
		if (x > 0 && y > 0) {
			synchronized (lieux[x][y]) {
				for (Chose chose : lieux[x][y]) {
					if (chose instanceof Vegetal)
						retVal = (Vegetal) chose;
				}
			}
		}

		return retVal;
	}

	public void trouveUnePlaceAletoire(Chose choseAPlacer) {
		Coordonnee coordonnee;
		do {
			int x = (int) (Math.random() * lieux[0].length);
			int y = (int) (Math.random() * lieux.length);
			coordonnee = new Coordonnee(x, y);
		} while (!place(choseAPlacer, coordonnee));
	}

	/**
	 * Trouve une place disponible près de la coordonnée reçue en paramètre.
	 * Retourne l s'il n'y plus de place disponible. (Méthode non asynchrone)
	 * 
	 * @param choseAPlacer
	 *            le nouvel objet à placer
	 * @param coordonneeDepart
	 *            La position autour de laquelle on doit placer l'objet
	 * @return la nouvelle coordonnée.
	 */
	public Coordonnee trouveUnePlacePresDe(Chose chose) {
		int direction;
		Terrain terrain = Terrain.getInstance();
		Coordonnee coordonneeInitiale = chose.getCoordonnees();
		Coordonnee nouvelleCoordonnee = null;

		Sequence sequence = new RandomSansRepetition(
				Coordonnee.NOMBRE_DE_DIRECTION);
		int essaiMax = NOMBRE_DE_RECHERCHE_SEQUENCE;
		do {
			direction = sequence.nextNumber();
			nouvelleCoordonnee = coordonneeInitiale.retourneACote(direction);
			essaiMax--;
			// S'il n'y a pas de place, la coordonnée ne change pas.
		} while (!terrain.estPlacable(chose, nouvelleCoordonnee)
				&& !nouvelleCoordonnee.equals(coordonneeInitiale)
				&& essaiMax > 0);

		if (nouvelleCoordonnee.equals(coordonneeInitiale)) {
			nouvelleCoordonnee = null;
		}
		return nouvelleCoordonnee;
	}

	/**
	 * PLace l'objet près d'un autre si possible. (Méthode asynchrone)
	 * 
	 * @param plante
	 *            la plante proche
	 * @param vegetal
	 *            la plante à placer
	 * @return vrai si la plante a été placée
	 */
	public boolean placePresDe(Vegetal plante, Vegetal vegetal) {

		Sequence sequence = new RandomSansRepetition(
				Coordonnee.NOMBRE_DE_DIRECTION);
		Coordonnee coordonneeInitiale = plante.getCoordonnees();
		Coordonnee nouvelleCoordonnee = null;
		Terrain terrain = Terrain.getInstance();

		int direction;
		int essaiMax = NOMBRE_DE_RECHERCHE_SEQUENCE;
		boolean estPlace = false;
		do {
			direction = sequence.nextNumber();
			nouvelleCoordonnee = coordonneeInitiale.retourneACote(direction);
			int x = nouvelleCoordonnee.getX();
			int y = nouvelleCoordonnee.getY();
			if (x >= 0 && y >= 0 && x < lieux[0].length && y < lieux.length) {
				synchronized (lieux[x][y]) {
					estPlace = terrain.place(vegetal, nouvelleCoordonnee);
				}
			}
			essaiMax--;
			// S'il n'y a pas de place, la coordonnée ne change pas.
		} while (!estPlace && essaiMax > 0);
		return estPlace;
	}

	/**
	 * Retirne une chose du terrain
	 * 
	 * @param chose
	 *            la chose à retirer
	 */
	public <T extends Chose> void retireChose(T chose) {
		// Les coordonnee actuelles sont à (-1,-1) lorsque le vivant est mort
		Coordonnee coordonnee = chose.getCoordonnees();
		int x = coordonnee.getX();
		int y = coordonnee.getY();

		lieux[x][y].remove(chose);
		chose.setCoordonnees(new Coordonnee(-1, -1));
	}

	/**
	 * Convertit la position (x,y) en index. L'index représente la position de
	 * la coordonnée sur un tableau unidimensionnel. ex: avec un tableau 3x3 les
	 * coordonnées donneront les indices suivants: 012 345 678
	 * 
	 * @param x
	 *            la position horizontale
	 * @param y
	 *            la position verticale
	 * @return l'indice
	 */
	public static int convertCoord(int x, int y) {
		return y * singleton.lieux[0].length + x;
	}

	/**
	 * Convertit la coordonnée reçue en index. L'index représente la position de
	 * la coordonnée sur un tableau unidimensionnel. ex: avec un tableau 3x3 les
	 * coordonnées donneront les indices suivants: 012 345 678
	 * 
	 * @param x
	 *            la position horizontale
	 * @param y
	 *            la position verticale
	 * @return l'indice
	 */
	public static int convertCoord(Coordonnee coordonnee) {
		return convertCoord(coordonnee.getX(), coordonnee.getY());
	}

	/**
	 * Convertit l'index en coordonnée.
	 * 
	 * @param index
	 *            l'index à convertir
	 * @return la coordonnée.
	 */
	public static Coordonnee convert(int index) {
		assert index >= 0 : "Index négatif";

		int dimensionX = singleton.lieux[0].length;
		int y = index / dimensionX;
		int x = index - (y * dimensionX);
		return new Coordonnee(x, y);
	}

	@Override
	public String toString() {
		String retVal = "";
		Chose chose;
		for (int y = 0; y < lieux.length; y++) {
			for (int x = 0; x < lieux[0].length; x++) {
				Stack<Chose> stack = singleton.get(x, y);
				if (!stack.empty()) {
					chose = singleton.get(x, y).peek();
					retVal += chose.getName().charAt(0);
				} else {
					retVal += "s";
				}
			}
			retVal += "\n";
		}
		return retVal;
	}

}
